// FacebookApplication.cs
// Facebook/FrameworkWeb/Web
// Copyright (c) 2007, Nikhil Kothari. All Rights Reserved.
//

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Web;
using System.Web.UI;
using Facebook.Service;
using Facebook.Service.Core;
using Facebook.Web.Configuration;

// TODO: Update DefaultProperty to be ApplicationName
//       Add designer support for ApplicationName property

namespace Facebook.Web {

    /// <summary>
    /// Represents a Facebook application. This control manages establishing a session
    /// and provides access to the Facebook APIs.
    /// Only one instance of this control can be placed on the page.
    /// </summary>
    [
    ControlValueProperty("UserID"),
    DefaultEvent("SessionCreated"),
    DefaultProperty("ApplicationKey"),
    Designer(typeof(Facebook.Design.Web.FacebookApplicationDesigner)),
    NonVisualControl
    ]
    public class FacebookApplication : Control {

        private static readonly object SessionCreatingEvent = new object();
        private static readonly object SessionCreatedEvent = new object();

        private FacebookApplicationMode _mode;
        private FacebookSession _session;
        private FacebookService _service;
        private bool _enableDebugging;
        private bool _enableExternalBrowsing;
        private bool _enableAjaxScenarios;

        private string _applicationName;
        private string _applicationKey;
        private string _secret;

        private object _controlState;

        private bool _isAppAdded;
        private bool _inCanvasPage;
        private bool _inIFramePage;

        /// <summary>
        /// The application key used to uniquely identify the application that is available
        /// at application registration time.
        /// This can only be set during initialization of the page.
        /// </summary>
        [
        Category("Behavior"),
        DefaultValue(""),
        Description("The application key that is used to identify a Facebook application.")
        ]
        public string ApplicationKey {
            get {
                if (_applicationKey == null) {
                    return String.Empty;
                }
                return _applicationKey;
            }
            set {
                if (_session != null) {
                    throw new InvalidOperationException("ApplicationKey can only be set during page initialization.");
                }
                _applicationKey = value;
            }
        }

        /// <summary>
        /// The name of the application. This is used to create the application's canvas
        /// page URL, i.e. http://apps.facebook.com/[name]/.
        /// On the server, this is used to load settings from configuration. Specifically,
        /// the facebook section in configuration is looked up for an application object
        /// with the given name.
        /// </summary>
        [
        Category("Behavior"),
        DefaultValue(""),
        Description("The application name that is used to load Application Key and Secret from configuration.")
        ]
        public string ApplicationName {
            get {
                if (_applicationName == null) {
                    return String.Empty;
                }
                return _applicationName;
            }
            set {
                if (_session != null) {
                    throw new InvalidOperationException("ApplicationName can only be set during page initialization.");
                }
                _applicationName = value;
            }
        }

        /// <summary>
        /// Gets the configuration settings for this application if the ApplicationName
        /// property is set.
        /// </summary>
        [Browsable(false)]
        public FacebookApplicationSettings ApplicationSettings {
            get {
                if (String.IsNullOrEmpty(_applicationName) == false) {
                    return FacebookSection.GetApplication(_applicationName);
                }
                return null;
            }
        }

        /// <summary>
        /// Whether to enable rendering of Ajax support functionality in IFrame
        /// pages.
        /// Specifically it adds the fbContext global variable which identifies the
        /// application and session information, which may be consumed by client
        /// applications to use when invoking the FacebookProxy.axd service. The
        /// path to the proxy itself is written out as the fbProxy global variable.
        /// You must set ApplicationName in order to use this feature.
        /// </summary>
        [
        Category("IFrame Behavior"),
        DefaultValue(false),
        Description("Enable rendering of Ajax support features.")
        ]
        public bool EnableAjaxScenarios {
            get {
                return _enableAjaxScenarios;
            }
            set {
                _enableAjaxScenarios = value;
            }
        }

        /// <summary>
        /// Whether to enable debug information to be sent as part of the response in
        /// the case of an error.
        /// This can only be set during initialization of the page.
        /// </summary>
        [
        Category("FBML Behavior"),
        DefaultValue(false),
        Description("Enables debugging features for FBML applications.")
        ]
        public bool EnableDebugging {
            get {
                return _enableDebugging;
            }
            set {
                if (_session != null) {
                    throw new InvalidOperationException("EnableDebugging can only be set during page initialization.");
                }
                _enableDebugging = value;
            }
        }

        /// <summary>
        /// Whether to enable IFrame pages to be browsed externally as a regular Web application, i.e.,
        /// outside Facebook when a particular user's session information is not available.
        /// This can only be set during initialization of the page.
        /// </summary>
        [
        Category("IFrame Behavior"),
        DefaultValue(false),
        Description("Enable regular browsing of this page external to Facebook.")
        ]
        public bool EnableExternalBrowsing {
            get {
                return _enableExternalBrowsing;
            }
            set {
                if (_session != null) {
                    throw new InvalidOperationException("RenderExternalBrowsing can only be set during page initialization.");
                }
                _enableExternalBrowsing = value;
            }
        }

        /// <internalonly />
        [
        Browsable(false),
        DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
        EditorBrowsable(EditorBrowsableState.Never)
        ]
        public override bool EnableTheming {
            get {
                return false;
            }
            set {
                throw new NotSupportedException();
            }
        }

        /// <internalonly />
        [
        Browsable(false),
        DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
        EditorBrowsable(EditorBrowsableState.Never)
        ]
        public override bool EnableViewState {
            get {
                return false;
            }
            set {
                throw new NotSupportedException();
            }
        }

        /// <summary>
        /// Whether the application has been added by the current user.
        /// </summary>
        [Browsable(false)]
        public bool IsApplicationAdded {
            get {
                return _isAppAdded;
            }
        }

        /// <summary>
        /// Whether the request is being made in the context of a canvas page.
        /// </summary>
        [Browsable(false)]
        public bool InCanvasPage {
            get {
                return _inCanvasPage;
            }
        }

        /// <summary>
        /// Whether the request is being made in the context of an iframe page.
        /// </summary>
        [Browsable(false)]
        public bool InIFramePage {
            get {
                return _inIFramePage;
            }
        }

        /// <summary>
        /// Indicates whether a Facebook session has been established or not.
        /// </summary>
        [Browsable(false)]
        public bool IsSessionCreated {
            get {
                return (_session != null);
            }
        }

        /// <summary>
        /// The type of user interface used by the Facebook application.
        /// This can only be set during initialization of the page.
        /// </summary>
        [
        Category("Behavior"),
        DefaultValue(FacebookApplicationMode.IFrame),
        Description("The type of Facebook application.")
        ]
        public FacebookApplicationMode Mode {
            get {
                return _mode;
            }
            set {
                if ((value < FacebookApplicationMode.IFrame) || (value > FacebookApplicationMode.Fbml)) {
                    throw new ArgumentOutOfRangeException("value");
                }
                if (_session != null) {
                    throw new InvalidOperationException("Mode can only be set during page initialization.");
                }
                _mode = value;
            }
        }

        /// <summary>
        /// The ID of the user browsing the page that the application is associated with.
        /// </summary>
        [Browsable(false)]
        public string PageUserID {
            get {
                return Session.PageUserID;
            }
        }

        /// <summary>
        /// The application secret used to authenticate API calls issued by the application that is
        /// available at application registration time.
        /// This can only be set during initialization of the page.
        /// </summary>
        [
        Category("Behavior"),
        DefaultValue(""),
        Description("The application secret that is used to authenticate API calls.")
        ]
        public string Secret {
            get {
                if (_secret == null) {
                    return String.Empty;
                }
                return _secret;
            }
            set {
                if (_session != null) {
                    throw new InvalidOperationException("Secret can only be set during page initialization.");
                }
                _secret = value;
            }
        }

        /// <summary>
        /// The Facebook service providing access to the Facebook APIs.
        /// This is only available once a Facebook session has been established.
        /// </summary>
        [Browsable(false)]
        public FacebookService Service {
            get {
                if (_service == null) {
                    throw new InvalidOperationException("Service can only be accessed after a Facebook session is established.");
                }
                return _service;
            }
        }

        /// <summary>
        /// The current Facebook session if one has been established. A session
        /// is established after page initialization is complete. The SessionCreated
        /// event signals successful creation of a session.
        /// </summary>
        [Browsable(false)]
        public FacebookSession Session {
            get {
                if (_session == null) {
                    throw new InvalidOperationException("Session can only be accessed after a Facebook session is established.");
                }
                return _session;
            }
        }

        /// <summary>
        /// The type of the active session.
        /// </summary>
        [Browsable(false)]
        public FacebookSessionType SessionType {
            get {
                return Session.SessionType;
            }
        }

        /// <summary>
        /// The unique ID of the user associated with the current Facebook session.
        /// If a Facebook session has not been established, it returns an empty string.
        /// If the session is associated with a Facebook Page, this returns the
        /// ID of the Page instead. In that scenario, the ID of the user itself is
        /// accessible via the PageUserID property.
        /// </summary>
        [Browsable(false)]
        public string UserID {
            get {
                if (_service == null) {
                    return String.Empty;
                }
                return _service.UserID;
            }
        }

        /// <summary>
        /// Whether the control is visible. A FacebookApplication control should always be
        /// set to visible.
        /// </summary>
        [
        Browsable(false),
        DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
        EditorBrowsable(EditorBrowsableState.Never)
        ]
        public override bool Visible {
            get {
                return base.Visible;
            }
            set {
                if (value == false) {
                    throw new NotSupportedException();
                }
                base.Visible = value;
            }
        }

        /// <summary>
        /// Raised when a Facebook session has been created.
        /// </summary>
        [
        Category("Behavior"),
        Description("Raised whan a Facebook session has been created, and the Facebook service can be used.")
        ]
        public event EventHandler SessionCreated {
            add {
                Events.AddHandler(SessionCreatedEvent, value);
            }
            remove {
                Events.RemoveHandler(SessionCreatedEvent, value);
            }
        }

        /// <summary>
        /// Raised when a Facebook session is being created.
        /// This event is only raised in IFrame apps. It is useful for
        /// directing FacebookApplication to create a session using an infinite
        /// key when the page is being browsed externally, i.e. outside
        /// of Facebook, by virtue of setting EnableExternalBrowsing to true.
        /// </summary>
        [
        Category("Behavior"),
        Description("Raised whan a Facebook session is being created.")
        ]
        public event FacebookSessionCreatingEventHandler SessionCreating {
            add {
                Events.AddHandler(SessionCreatingEvent, value);
            }
            remove {
                Events.RemoveHandler(SessionCreatingEvent, value);
            }
        }

        /// <summary>
        /// Gets the current FacebookApplication control instance on the page.
        /// </summary>
        /// <param name="page">The page that contains the control; should be the current page instance.</param>
        /// <returns>The FacebookApplication instance if there is one; null otherwise.</returns>
        public static FacebookApplication GetCurrent(Page page) {
            return page.Items[typeof(FacebookApplication)] as FacebookApplication;
        }

        /// <summary>
        /// Loads the state of the control.
        /// </summary>
        /// <param name="savedState">The previously saved state.</param>
        protected override void LoadControlState(object savedState) {
            _controlState = savedState;
        }

        /// <summary>
        /// Initializes the control, and registers it with the page as the current
        /// FacebookApplication instance.
        /// </summary>
        /// <param name="e"></param>
        protected override void OnInit(EventArgs e) {
            base.OnInit(e);

            if (DesignMode) {
                return;
            }

            FacebookApplication registeredApp = GetCurrent(Page);
            if (registeredApp != null) {
                throw new InvalidOperationException("You can only have one instance of a FacebookApplication control on the page.");
            }

            Page.Items[typeof(FacebookApplication)] = this;
            Page.PreLoad += OnPagePreLoad;
            Page.Error += OnPageError;

            Page.RegisterRequiresControlState(this);

            HttpRequest request = Context.Request;
            _isAppAdded = (String.CompareOrdinal(request["fb_sig_added"], "1") == 0) ||
                          (String.IsNullOrEmpty(request["fb_page_id"]) == false);
            _inCanvasPage = (String.CompareOrdinal(request["fb_sig_in_canvas"], "1") == 0);
            _inIFramePage = (String.CompareOrdinal(request["fb_sig_in_iframe"], "1") == 0);
        }

        private void OnPageError(object sender, EventArgs e) {
            if (_mode != FacebookApplicationMode.Fbml) {
                return;
            }

            HttpRequest request = Page.Request;
            HttpResponse response = Page.Response;

            response.Write("<div style=\"padding: 10px\">");
            if (_enableDebugging == false) {
                response.Write("<p>Sorry, there was an error processing the request. Please refresh the page to try again.</p>");
            }
            else {
                response.Write("<h1>Exception Information</h1>");
                Exception ex = Context.Error;
                if (ex != null) {
                    response.Write("<p>");
                    response.Write(ex.Message);
                    response.Write("</p>");
                    response.Write("<pre>");
                    response.Write(ex.StackTrace);
                    response.Write("</pre>");
                }
                else {
                    response.Write("<p>Unknown error</p>");
                }

                response.Write("<hr />");

                response.Write("<h1>Request Form Data</h1>");
                response.Write("<pre>");
                foreach (string s in request.Form) {
                    response.Write(s);
                    response.Write(" = ");
                    response.Write(request.Form[s]);
                    response.Write("<br />");
                }
                response.Write("</pre>");
            }

            response.Write("</div>");

            Context.ClearError();
            response.End();
        }

        private void OnPagePreLoad(object sender, EventArgs e) {
            if (String.IsNullOrEmpty(_applicationKey) || String.IsNullOrEmpty(_secret)) {
                string appName = ApplicationName;

                if (appName.Length != 0) {
                    FacebookApplicationSettings appSettings = ApplicationSettings;

                    if (appSettings != null) {
                        _applicationKey = appSettings.ApiKey;
                        _secret = appSettings.Secret;

                        Debug.Assert(String.IsNullOrEmpty(_applicationKey) == false);
                        Debug.Assert(String.IsNullOrEmpty(_secret) == false);
                    }
                    else {
                        throw new InvalidOperationException("The specified application name is not a valid application registered in configuration.");
                    }
                }
            }

            if (String.IsNullOrEmpty(_applicationKey)) {
                throw new InvalidOperationException("The ApplicationKey must be set during page initialization.");
            }
            if (String.IsNullOrEmpty(_secret)) {
                throw new InvalidOperationException("The Secret must be set during page initialization.");
            }

            FacebookSessionCreatingEventArgs creatingEventArgs = new FacebookSessionCreatingEventArgs();
            if ((_mode == FacebookApplicationMode.IFrame) && (InIFramePage == false)) {
                OnSessionCreating(creatingEventArgs);
            }

            if (creatingEventArgs.SessionKey != null) {
                // Create a service using the developer supplied session information.
                // Creating the service with this information creates an infinite session object
                // under the covers, which we'll pull out and hang onto, rather than
                // create an actual server session object.
                // This is useful in the external browsing scenario.

                _service = new FacebookService(_applicationKey, _secret,
                                               creatingEventArgs.SessionKey,
                                               creatingEventArgs.UserID, creatingEventArgs.PageUserID);
                _session = _service.Session;

                OnSessionCreated(EventArgs.Empty);
            }
            else {
                FacebookServerSession session;
                if (_mode == FacebookApplicationMode.IFrame) {
                    session = new FacebookIFrameSession(this, _applicationKey, _secret);
                    if (_controlState != null) {
                        session.LoadState(_controlState);
                        _controlState = null;
                    }
                }
                else {
                    session = new FacebookFbmlSession(this, _applicationKey, _secret);
                }

                string redirectUrl = session.Initialize(Context);
                if (redirectUrl == null) {
                    _session = session;
                    _service = new FacebookService(_session);
                    OnSessionCreated(EventArgs.Empty);
                }
                else {
                    if ((_mode != FacebookApplicationMode.IFrame) || InIFramePage ||
                        (_enableExternalBrowsing == false)) {
                        Redirect(redirectUrl, /* detectRelativeUrl */ false);
                    }
                }
            }
        }

        /// <summary>
        /// Prepares the control for rendering.
        /// </summary>
        /// <param name="e">The event information associated with the event.</param>
        protected override void OnPreRender(EventArgs e) {
            base.OnPreRender(e);

            if ((_mode == FacebookApplicationMode.IFrame) && InIFramePage) {
                Page.Response.AppendHeader("P3P", "CP=\"CAO PSA OUR\"");
            }
        }

        /// <summary>
        /// Raises the SessionCreated event upon successful creation of a Facebook session.
        /// </summary>
        /// <param name="e">The event information associated with the event.</param>
        protected virtual void OnSessionCreated(EventArgs e) {
            EventHandler handler = (EventHandler)Events[SessionCreatedEvent];
            if (handler != null) {
                handler(this, e);
            }
        }

        /// <summary>
        /// Raises the SessionCreating event before creation of a Facebook session.
        /// </summary>
        /// <param name="e">The event information associated with the event.</param>
        protected virtual void OnSessionCreating(FacebookSessionCreatingEventArgs e) {
            FacebookSessionCreatingEventHandler handler =
                (FacebookSessionCreatingEventHandler)Events[SessionCreatingEvent];
            if (handler != null) {
                handler(this, e);
            }
        }

        /// <summary>
        /// Performs a redirect appropriately based on the type of Facebook application.
        /// </summary>
        /// <param name="contextControl">The page or user control from within which the call is being made.</param>
        /// <param name="url">The URL to redirect to.</param>
        public void Redirect(Control contextControl, string url) {
            if (String.IsNullOrEmpty(url)) {
                throw new ArgumentNullException("url");
            }

            url = contextControl.ResolveClientUrl(url);
            Redirect(url, /* detectRelativeUrl */ true);
        }

        private void Redirect(string url, bool detectRelativeUrl) {
            Debug.Assert(String.IsNullOrEmpty(url) == false);

            if ((_session != null) &&
                (_session.SessionType == FacebookSessionType.PresencePage)) {
                int queryIndex = url.IndexOf('?');
                url += (queryIndex >= 0) ? "&" : "?";
                url += "fb_page_id=" + UserID;
            }

            HttpResponse response = Page.Response;
            if (InCanvasPage) {
                response.Write("<fb:redirect url=\"" + url + "\" />");
            }
            else if (InIFramePage) {
                if (detectRelativeUrl) {
                    Debug.Assert(_session is FacebookIFrameSession);
                    url = ((FacebookIFrameSession)_session).ResolveUrl(Context, url);
                }

                response.Write(String.Format(@"<script type=""text/javascript"">window.top.location.href='{0}';</script>", url));
            }
            else {
                // A regular HTTP redirect. This really only makes sense if the
                // application is an IFrame application, and its being browsed
                // externally.
                response.Redirect(url);
            }
            response.End();
        }

        /// <summary>
        /// Renders the control into markup.
        /// </summary>
        /// <param name="writer">The writer into which the markup is rendered.</param>
        protected override void Render(HtmlTextWriter writer) {
            if ((_mode == FacebookApplicationMode.IFrame) && InIFramePage) {
                if (_enableAjaxScenarios && (ApplicationName.Length == 0)) {
                    throw new InvalidOperationException("You must set ApplicationName in order to set EnableAjaxScenarios.");
                }

                string script = String.Format(@"<script type=""text/javascript"">window.fbContext = '{0};{1};{2};{3}'; window.fbUserID = '{2}'; window.fbPageUserID = '{3}'; window.fbProxy = '{4}';</script>",
                                              ApplicationName, _session.SessionKey, _session.UserID, _session.PageUserID,
                                              ResolveClientUrl("~/FacebookProxy.axd"));
                writer.Write(script);
            }
        }

        /// <summary>
        /// Saves control state associated with the control.
        /// </summary>
        /// <returns>The state of the control to be preserved.</returns>
        protected override object SaveControlState() {
            if ((_session != null) && (_session is FacebookIFrameSession)) {
                return ((FacebookIFrameSession)_session).SaveState();
            }
            return null;
        }
    }
}
